/**
 * 
 */
package neptune.manager.storage;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import neptune.manager.model.topology.Cpu;
import neptune.manager.model.topology.DuplicatedNodeException;
import neptune.manager.model.topology.EndPoint;
import neptune.manager.model.topology.IPAddress;
import neptune.manager.model.topology.Link;
import neptune.manager.model.topology.LinkException;
import neptune.manager.model.topology.MACAddress;
import neptune.manager.model.topology.NetworkInterface;
import neptune.manager.model.topology.Node;
import neptune.manager.model.topology.OLPIEndPoint;
import neptune.manager.model.topology.OperativeSystem;
import neptune.manager.model.topology.Property;
import neptune.manager.model.topology.Software;
import neptune.manager.model.topology.Topology;
import neptune.manager.utils.DebugLog;
import neptune.manager.utils.IllegalValueException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * @author Roberto Bifulco [info@robertobifulco.it, robertobifulco.it]
 * @author Luigi Lorenzo Arpino [luigiarpino@yahoo.it]
 * 
 */
public class XMLTopologyStorage implements TopologyStorage {

	private File xmlFile;
	private File tempXmlFile;

	/**
	 * @param xmlFile
	 */
	public XMLTopologyStorage(File xmlFile, File tempXmlFile) {
		this.xmlFile = xmlFile;
		this.tempXmlFile = tempXmlFile;
	}

	@Override
	public Topology getTopology() throws StorageReadException,
	TopologyDefinitionException {
		return getTopologyFromFile(xmlFile);
	}
	
	private Topology getTopologyFromFile(File file) throws StorageReadException,
			TopologyDefinitionException {
		boolean error = false;
		String errorMessage = "";
		Topology topology = new Topology();

		DocumentBuilderFactory docFactory = DocumentBuilderFactory
				.newInstance();
		docFactory.setValidating(false); // La validazione e' intesa solo in
		// riferimento ai DTD

		// TODO inserire riferimento a XML SCHEMA
		// docFactory.setSchema(schema)

		try {
			Document doc = docFactory.newDocumentBuilder().parse(file);

			Element topologyEl = doc.getDocumentElement();
			topology.setName(topologyEl.getAttribute("id"));

			NodeList nodes = topologyEl.getElementsByTagName("node");
			for (int i = 0; i < nodes.getLength(); i++) {
				Node node = buildNodeFromElement((Element) nodes.item(i));
				try {
					topology.addNode(node);
				} catch (DuplicatedNodeException e) {
					error = false;
					errorMessage += e.getMessage() + "\n";
				}
			}

			NodeList links = topologyEl.getElementsByTagName("link");
			for (int i = 0; i < links.getLength(); i++) {
				Link link = buildLinkFromElement((Element) links.item(i));
				try {
					topology.addLink(link);
				} catch (LinkException e) {
					error = true;
					errorMessage += e.getMessage() + "\n";
				}
			}

		} catch (SAXException e) {
			throw new StorageReadException(e);
		} catch (IOException e) {
			DebugLog.printDetailedLog(
					"Topology read problem:" + e.getMessage(),
					XMLTopologyStorage.class.getSimpleName());
			return new Topology();
		} catch (ParserConfigurationException e) {
			throw new StorageReadException(e);
		}

		if (error)
			throw new TopologyDefinitionException(errorMessage);

		return topology;
	}

	private static Link buildLinkFromElement(Element item) {
		String linkId = item.getAttribute("id");
		String linkType = item.getAttribute("type");
		boolean isFullDuplex = false;
		if (linkType.equals("fullDuplex"))
			isFullDuplex = true;

		EndPoint source = null;
		EndPoint destination = null;

		NodeList endPoints = item.getElementsByTagName("end_point");
		for (int i = 0; i < endPoints.getLength(); i++) {
			Element endPointEl = (Element) endPoints.item(i);
			EndPoint ep = buildEndPointFromElement(endPointEl);

			if (endPointEl.getAttribute("type").equals("source")) {
				source = ep;
			} else {
				destination = ep;
			} // TODO modificare qui se si prevedono tipi diversi da source e
			// destination
		}

		Link link = new Link(linkId, source, destination, isFullDuplex);

		NodeList props = item.getElementsByTagName("property");
		for (int i = 0; i < props.getLength(); i++) {
			Element propEl = (Element) props.item(i);
			Property prop = new Property(propEl.getAttribute("name"), propEl
					.getAttribute("value"));
			link.addProperty(prop);
		}

		return link;
	}

	private static EndPoint buildEndPointFromElement(Element item) {
		String nodeId = item.getAttribute("nodeId");

		if (item.getElementsByTagName("multiplexing").item(0) != null) {

			Element multiplexing = (Element) item.getElementsByTagName(
					"multiplexing").item(0);

			if (multiplexing.getAttribute("tech").equals("OLPI")) {
//				MACAddress address = null;
				String interfaceName = null;

//				if (item.getElementsByTagName("macAddress").item(0) != null) {
//					Element macAddressEl = (Element) item.getElementsByTagName(
//							"macAddress").item(0);
//					address = new MACAddress(macAddressEl.getAttribute("value"));
//				} else {
//					// TODO inserire cosa fare se non e' definito il mac address
//				}

				if (item.getElementsByTagName("interface").item(0) != null) {
					Element el = (Element) item.getElementsByTagName(
							"interface").item(0);
					interfaceName = el.getAttribute("name");
				} else {
					// TODO inserire cosa fare se non definita
				}

				return new OLPIEndPoint(nodeId, interfaceName);

			} else {
				// TODO inserire cosa fare se non e' un end point OLPI
				return null;
			}

		} else {
			// TODO inserire cosa fare se non e' definita la tecnica di
			// multiplexing
			return null;
		}
	}

	private static Node buildNodeFromElement(Element item)
			throws TopologyDefinitionException {

		NodeList descriptionNL = item.getElementsByTagName("description");
		String description = "";
		if (descriptionNL.item(0) != null)
			description = descriptionNL.item(0).getTextContent();
		
		NodeList nameNL = item.getElementsByTagName("name");
		String name="";
		if (nameNL.item(0) != null )
			name = nameNL.item(0).getTextContent();
		
		Node node = new Node(description, item.getAttribute("id"), name);
		

		NodeList itemChilds = item.getChildNodes();
		for (int i = 0; i < itemChilds.getLength(); i++) {
			if (!itemChilds.item(i).getNodeName().equals("#text")) {

				String nodeName = itemChilds.item(i).getNodeName();

				if (nodeName.equals("cpu")) {

					Element cpuEl = (Element) itemChilds.item(i);
					Cpu cpu = new Cpu(cpuEl.getAttribute("name"), cpuEl
							.getAttribute("type"), Integer.parseInt(cpuEl
							.getAttribute("clock")), Integer.parseInt(cpuEl.getAttribute("amount")));
					node.setCpu(cpu);

				} else if (nodeName.equals("ram")) {

					Element ramEl = (Element) itemChilds.item(i);
					node.setRam(Integer.parseInt(ramEl
							.getAttribute("dimension")));

				} else if (nodeName.equals("hd")) {

					Element hdEl = (Element) itemChilds.item(i);
					node
							.setHd(Float.parseFloat(hdEl
									.getAttribute("dimension")));

				} else if (nodeName.equals("network_interfaces")) {

					NodeList interfaces = ((Element) itemChilds.item(i))
							.getElementsByTagName("interface");
					for (int j = 0; j < interfaces.getLength(); j++) {
						try {
							NetworkInterface netInterface = buildInterfaceFromElement((Element) interfaces
									.item(j));
							node.addNetworkInterface(netInterface);
						} catch (IllegalValueException e) {
							throw new TopologyDefinitionException(e);
						}
					}

				} else if (nodeName.equals("operative_system")) {

					Element osEl = (Element) itemChilds.item(i);
					OperativeSystem os = new OperativeSystem(osEl
							.getAttribute("name"),
							osEl.getAttribute("version"), osEl
									.getAttribute("username"), osEl
									.getAttribute("password"));
					node.setOperativeSystem(os);

				} else if (nodeName.equals("software_configuration")) {

					Element softwareConfigurationEl = (Element) itemChilds
							.item(i);
					node.setSoftwareTemplate(softwareConfigurationEl
							.getAttribute("template"));

					Element sshServer = (Element) softwareConfigurationEl
							.getElementsByTagName("sshServer").item(0);
					node.setControlPort(Integer.parseInt(sshServer
							.getAttribute("port")));

					NodeList softwares = softwareConfigurationEl
							.getElementsByTagName("software");
					for (int j = 0; j < softwares.getLength(); j++) {
						Element softwareEl = (Element) softwares.item(j);
						node.addSoftware(new Software(softwareEl
								.getAttribute("name"), softwareEl
								.getAttribute("version"), softwareEl
								.getAttribute("type")));
					}

				} // TODO aggiungere elementi per nesting vm

			}
		}

		return node;
	}

	private static NetworkInterface buildInterfaceFromElement(Element item)
			throws IllegalValueException {
		Element bindingEl = (Element) item.getElementsByTagName(
				"interface_binding").item(0);

		NetworkInterface netInterface = new NetworkInterface(item
				.getAttribute("name"), bindingEl.getAttribute("type"),
				new MACAddress(item.getAttribute("MAC")));
		netInterface.setType(item.getAttribute("type"));

		NodeList ips = item.getElementsByTagName("ip");
		for (int i = 0; i < ips.getLength(); i++) {
			Element ipEl = (Element) ips.item(i);
			boolean isControl = false;
			if (ipEl.getAttribute("type").equals("control"))
				isControl = true;
			IPAddress ipAddress = new IPAddress(Integer.parseInt(ipEl
					.getAttribute("version")), ipEl.getAttribute("address"),
					ipEl.getAttribute("subnetMask"), isControl);
			netInterface.addIpAddress(ipAddress);
		}

		return netInterface;
	}

	
	/**
	 * Restituisce la descrizione XML della topologia 
	 * 
	 * @param topology
	 * @return
	 * @throws StorageSaveException
	 */
	public static String getXML(Topology topology) throws StorageSaveException 
	{
		DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
		docFactory.setValidating(false);

		try 
		{
			Document document = docFactory.newDocumentBuilder().newDocument();
			document.setXmlVersion("1.0");

			Element root = document.createElement("topology");
			root.setAttribute("id", topology.getName());
			document.appendChild(root);

			Element nodes = document.createElement("nodes");
			root.appendChild(nodes);
			Element links = document.createElement("links");
			root.appendChild(links);

			createNodesElement(document, topology, nodes);
			createLinksElement(document, topology, links);

			// ///////////////
			// Output the XML

			// set up a transformer
			TransformerFactory transfac = TransformerFactory.newInstance();
			Transformer trans = transfac.newTransformer();
			trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
			trans.setOutputProperty(OutputKeys.INDENT, "yes");
			trans.setOutputProperty(OutputKeys.STANDALONE, "yes");
			trans.setOutputProperty(
					"{http://xml.apache.org/xslt}indent-amount", "2");

			// create string from xml tree
			StringWriter sw = new StringWriter();
			StreamResult result = new StreamResult(sw);
			DOMSource source = new DOMSource(document);
			trans.transform(source, result);
			return sw.toString();

		} catch (ParserConfigurationException e) {
			throw new StorageSaveException(e);
		} catch (TransformerException e) {
			throw new StorageSaveException(e);
		}
	}

	@Override
	public void saveTopology(Topology topology) throws StorageSaveException
	{
		String xmlString = getXML(topology);
		try
		{
			xmlFile.createNewFile();
			FileWriter writer = new FileWriter(xmlFile);
			writer.write(xmlString);
			writer.flush();
		}
		catch (IOException e) 
		{
			throw new StorageSaveException(e);
		}
	}

	private static void createLinksElement(Document doc, Topology topology,
			Element links) {
		for (Link link : topology.getLinks()) {
			Element linkEl = doc.createElement("link");
			linkEl.setAttribute("id", link.getId());
			linkEl.setAttribute("type", link.isFullDuplex() ? "fullDuplex"
					: "halfDuplex");
			links.appendChild(linkEl);

			// SOURCE EP
			Element ep1 = doc.createElement("end_point");
			ep1.setAttribute("nodeId", link.getSource().getNodeId());
			ep1.setAttribute("type", "source");
			linkEl.appendChild(ep1);

			Element multipTech1 = doc.createElement("multiplexing");
			multipTech1.setAttribute("tech", Link.getMultiplexingTechLabel(link
					.getSource().getMultiplexingTech()));
			ep1.appendChild(multipTech1);

			Element epInterfaceEl1 = doc.createElement("interface");
			epInterfaceEl1.setAttribute("name", link.getSource()
					.getInterfaceId());
			ep1.appendChild(epInterfaceEl1);

//			if (link.getSource() instanceof OLPIEndPoint) {
//				Element epAddress = doc.createElement("macAddress");
//				epAddress.setAttribute("value", (((OLPIEndPoint) link
//						.getSource()).getAddress()).getAddress());
//				ep1.appendChild(epAddress);
//			}

			// DESTINATION EP
			Element ep2 = doc.createElement("end_point");
			ep2.setAttribute("nodeId", link.getDestination().getNodeId());
			ep2.setAttribute("type", "destination");
			linkEl.appendChild(ep2);

			Element multipTech2 = doc.createElement("multiplexing");
			multipTech2.setAttribute("tech", Link.getMultiplexingTechLabel(link
					.getDestination().getMultiplexingTech()));
			ep2.appendChild(multipTech2);

			Element epInterfaceEl2 = doc.createElement("interface");
			epInterfaceEl2.setAttribute("name", link.getDestination()
					.getInterfaceId());
			ep2.appendChild(epInterfaceEl2);

//			if (link.getSource() instanceof OLPIEndPoint) {
//				Element epAddress = doc.createElement("macAddress");
//				epAddress.setAttribute("value", (((OLPIEndPoint) link
//						.getDestination()).getAddress()).getAddress());
//				ep2.appendChild(epAddress);
//			}

			// Props
			for (Property prop : link.getProperties()) {
				if (prop != null) {
					Element propEl = doc.createElement("property");
					propEl.setAttribute("name", prop.getName());
					propEl.setAttribute("value", prop.getValue());
					linkEl.appendChild(propEl);
				}
			}
		}
	}

	/**
	 * 
	 * 
	 * @param doc
	 * @param topology
	 * @param nodesEl
	 */
	private static void createNodesElement(Document doc, Topology topology,
			Element nodesEl) {
		for (Node node : topology.getNodes())
		{
			Element nodeEl = doc.createElement("node");
			nodeEl.setAttribute("id", node.getId());
			nodesEl.appendChild(nodeEl);

			Element description = doc.createElement("description");
			description.setTextContent(node.getDescription());
			nodeEl.appendChild(description);
			
			
			Element name = doc.createElement("name");
			name.setTextContent(node.getName());
			nodeEl.appendChild(name);

			if (node.getCpu() != null) {
				Element cpu = doc.createElement("cpu");
				cpu.setAttribute("name", node.getCpu().getName());
				cpu.setAttribute("type", node.getCpu().getType());
				cpu.setAttribute("clock", Integer.toString(node.getCpu()
						.getClock()));
				cpu.setAttribute("amount", Integer.toString(node.getCpu().getAmount() ) );
				nodeEl.appendChild(cpu);
			}

			Element ram = doc.createElement("ram");
			ram.setAttribute("dimension", Integer.toString(node.getRam()));
			nodeEl.appendChild(ram);

			Element hd = doc.createElement("hd");
			hd.setAttribute("dimension", Float.toString(node.getHd()));
			nodeEl.appendChild(hd);

			Element networkIfs = doc.createElement("network_interfaces");
			nodeEl.appendChild(networkIfs);

			for (NetworkInterface netIf : node.getInterfaces()) {
				if (netIf == null)
					break;

				Element netIfEl = doc.createElement("interface");
				networkIfs.appendChild(netIfEl);
				netIfEl.setAttribute("name", netIf.getInterfaceName());
				netIfEl.setAttribute("MAC",
						netIf.getMacAddress() != null ? netIf.getMacAddress()
								.getAddress() : null);
				netIfEl.setAttribute("type", netIf.getType());

				Element ifBinding = doc.createElement("interface_binding");
				ifBinding.setAttribute("type", netIf.getBindingDescription());
				netIfEl.appendChild(ifBinding);

				for (IPAddress ipAddr : netIf.getIpAddresses()) {
					Element ipAddrEl = doc.createElement("ip");
					ipAddrEl.setAttribute("version", Integer.toString(ipAddr
							.getVersion()));
					ipAddrEl.setAttribute("address", ipAddr.getAddress());
					ipAddrEl.setAttribute("subnetMask", ipAddr.getSubnetMask());
					ipAddrEl.setAttribute("type",
							ipAddr.isControlAddress() ? "control" : "topology");
					netIfEl.appendChild(ipAddrEl);
				}
			}

			if (node.getOperativeSystem() != null) {
				Element os = doc.createElement("operative_system");
				os.setAttribute("name", node.getOperativeSystem().getName());
				os.setAttribute("version", node.getOperativeSystem()
						.getVersion());
				os.setAttribute("username", node.getOperativeSystem()
						.getAdminUsername());
				os.setAttribute("password", node.getOperativeSystem()
						.getAdminPassword());
				nodeEl.appendChild(os);
			}

			Element softConf = doc.createElement("software_configuration");
			softConf.setAttribute("template", node.getSoftwareTemplate());
			nodeEl.appendChild(softConf);

			Element sshElement = doc.createElement("sshServer");
			sshElement.setAttribute("port", Integer.toString(node
					.getControlPort()));
			softConf.appendChild(sshElement);

			for (Software soft : node.getInstalledSoftwares()) {
				Element softEl = doc.createElement("software");
				softEl.setAttribute("name", soft.getId());
				softEl.setAttribute("type", soft.getType());
				softEl.setAttribute("version", soft.getVersion());
				softConf.appendChild(softEl);
			}

			if (node.getHypervisor() != null) {
				Element hypervisor = doc.createElement("hypervisor");
				hypervisor.setAttribute("name", node.getHypervisor().getType());
				hypervisor.setAttribute("version", node.getHypervisor()
						.getVersion());
				nodeEl.appendChild(hypervisor);
			}
		}
	}

	public static Topology getTopologyFromXML(String xml)
			throws TopologyDefinitionException, StorageReadException {
		boolean error = false;
		String errorMessage = "";
		Topology topology = new Topology();

		DocumentBuilderFactory docFactory = DocumentBuilderFactory
				.newInstance();
		docFactory.setValidating(false); // La validazione e' intesa solo in
		// riferimento ai DTD

		// TODO inserire riferimento a XML SCHEMA
		// docFactory.setSchema(schema)

		try {
			Document doc = docFactory.newDocumentBuilder().parse(
					new ByteArrayInputStream(xml.getBytes()));

			Element topologyEl = doc.getDocumentElement();
			topology.setName(topologyEl.getAttribute("id"));

			NodeList nodes = topologyEl.getElementsByTagName("node");
			for (int i = 0; i < nodes.getLength(); i++) {
				Node node = buildNodeFromElement((Element) nodes.item(i));
				try {
					topology.addNode(node);
				} catch (DuplicatedNodeException e) {
					error = false;
					errorMessage += e.getMessage() + "\n";
				}
			}

			NodeList links = topologyEl.getElementsByTagName("link");
			for (int i = 0; i < links.getLength(); i++) {
				Link link = buildLinkFromElement((Element) links.item(i));
				try {
					topology.addLink(link);
				} catch (LinkException e) {
					error = true;
					errorMessage += e.getMessage() + "\n";
				}
			}

		} catch (SAXException e) {
			throw new StorageReadException(e);
		} catch (IOException e) {
			DebugLog.printDetailedLog(
					"Topology read problem:" + e.getMessage(),
					XMLTopologyStorage.class.getSimpleName());
			return new Topology();
		} catch (ParserConfigurationException e) {
			throw new StorageReadException(e);
		}

		if (error)
			throw new TopologyDefinitionException(errorMessage);

		return topology;
	}

	@Override
	public boolean deleteTopology()
	{
		boolean isRemoved=false;
		try
		{
			xmlFile.delete();
			isRemoved=true;
		}
		catch (Exception e) 
		{
			
		}
		return isRemoved;
	}

	@Override
	public void deleteTempTopology() {
		tempXmlFile.delete();
	}

	@Override
	public Topology getTempTopology() throws StorageReadException,
			TopologyDefinitionException {
		return getTopologyFromFile(tempXmlFile);
	}

	@Override
	public void saveTempTopology(Topology topology) throws StorageSaveException {
		String xmlString = getXML(topology);

		try {
			xmlFile.createNewFile();
			FileWriter writer = new FileWriter(tempXmlFile);
			writer.write(xmlString);
			writer.flush();
		} catch (IOException e) {
			throw new StorageSaveException(e);
		}
	}
}
